'use strict'

const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const fse = require('fs-extra')
const semver = require('semver')
const userHome = require('user-home')
const glob = require('glob')
const ejs = require('ejs')
const Command = require('@zy-cli/command')
const log = require('@zy-cli/log')
const Package = require('@zy-cli/package')
const { spinnerInit, sleep, execAsync }  =require('@zy-cli/utils')
const getProjectTemplate = require('./getProjectTemplate')

const TYPE_PROJECT = 'project'
const TYPE_COMPONENT = 'component'

const TEMPLATE_TYPE_NORMAL = 'normal'
const TEMPLATE_TYPE_CUSTOM = 'custom'

const WHITE_COMMAND = ['npm', 'cnpm']

class InitCommand extends Command {
    init() {
        this.projectName = this._argv[0] || ''
        this.force = !!this._cmd.force
        log.verbose('projectName', this.projectName)
        log.verbose('force', this.force)
    }

    async exec() {
        try {
            // 1. 准备阶段
            this.projectInfo = await this.prepare()
            if (this.projectInfo) {
                log.verbose('projectInfo', this.projectInfo)
                // 2. 下载模板
                await this.downLoadTemplate()
                // 3. 安装模板
                await this.installTemplate()
            }
        } catch (e) {
            log.error(e.message)
            if (process.env.LOG_LEVEL === 'verbose') {
                console.log(e)
            }
        }
    }

    async prepare() {
        // 判断项目模板是否存在
        const template = await getProjectTemplate()
        if (!template || !template.length) {
            throw new Error('项目模板不存在')
        }
        this.template = template
        const localPath = process.cwd()
        // 1. 判断目录是否存在
        let ifContinue = false
        if (!this.isDirEmpty(localPath)) {
            if (!this.force) {
                // 如果当前目录不为空
                ifContinue = (await inquirer.prompt({
                    type: 'confirm',
                    name: 'ifContinue',
                    default: false,
                    message: '当前文件夹不为空，是否继续创建项目？'
                })).ifContinue
                if (!ifContinue) {
                    return
                }
            }
            // 2. 是否强制更新
            if (ifContinue || this.force) {
                // 二次确认
                const { confirmDelete } = await inquirer.prompt({
                    type: 'confirm',
                    name: 'confirmDelete',
                    default: false,
                    message: '是否确认清空当前目录下的文件？'
                })
                if (confirmDelete) {
                    // 清空当前文件夹
                    fse.emptyDirSync(localPath)
                }
            }
        }
        return this.getProjectInfo()
    }

    async installTemplate() {
        if (!this.templateInfo) {
            throw new Error('项目模板信息不存在')
        }
        if (!this.templateInfo.type) {
            this.templateInfo.type = TEMPLATE_TYPE_NORMAL
        }
        if (![TEMPLATE_TYPE_NORMAL, TEMPLATE_TYPE_CUSTOM].includes(this.templateInfo.type)) {
            throw new Error('无法识别项目模板类型')
        }
        // 标准安装
        if (this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
            await this.installNormalTemplate()
            return
        }
        // 自定义安装
        if (this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
            await this.installCustomTemplate()
            return
        }
    }

    async execCommand(command, errMsg) {
        let ret
        if (command) {
            const cmdArray = command.split(' ')
            const cmd = this.checkCommand(cmdArray[0])
            const args = cmdArray.slice(1)
            ret = await execAsync(cmd, args, {
                stdio: 'inherit',
                cwd: process.cwd()
            })
        }
        if (ret !== 0) {
            throw new Error(errMsg)
        }
        return ret
    }

    async installNormalTemplate() {
        log.verbose('templateInfo', this.templateInfo)
        const spinner = spinnerInit('正在安装模板...')
        await sleep()
        try {
            // 拷贝模板至当前目录
            const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
            const targetPath = process.cwd()
            fse.ensureDirSync(templatePath)
            fse.ensureDirSync(targetPath)
            fse.copySync(templatePath, targetPath)
        } catch (error) {
            throw error
        } finally {
            spinner.stop(true)
            log.success('模板安装成功')
        }
        const templateIgnore = this.templateInfo.ignore || []
        const ignore = ['**/node_modules/**', ...templateIgnore]
        await this.ejsRender({ignore})
        const { installCommand, startCommand } = this.templateInfo
        // 安装依赖
        await this.execCommand(installCommand, '依赖安装失败')
        // 启动项目
        await this.execCommand(startCommand, '项目启动失败')
    }

    async installCustomTemplate() {
        // 查询自定义模板的入口文件
        if (await this.templateNpm.exists()) {
            const rootFile = this.templateNpm.getRootFilePath()
            if (!fs.existsSync(rootFile)) {
                throw new Error('自定义模板入口文件不存在')
            }
            log.notice('开始执行自定义模板')
            const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
            const options = {
                templateInfo: this.templateInfo,
                projectInfo: this.projectInfo,
                sourcePath: templatePath,
                targetPath: process.cwd()
            }
            const code = `require('${rootFile}')(${JSON.stringify(options)})`
            log.verbose('code', code)
            await execAsync('node', ['-e', code], { stdio: 'inherit', cwd: process.cwd() })
            log.success('自定义模板安装成功')
        }
    }

    async getProjectInfo() {
        const isValidName = v => /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v)
        let projectInfo = {}
        // 1. 选择创建项目还是组件
        const { type } = await inquirer.prompt({
            type: 'list',
            name: 'type',
            message: '请选择项目类型',
            default: TYPE_PROJECT,
            choices: [
                { name: '项目', value: TYPE_PROJECT },
                { name: '组件', value: TYPE_COMPONENT }
            ]
        })
        log.verbose('type', type)
        const title = type === TYPE_PROJECT ? '项目' : '组件'
        this.template = this.template.filter(item => item.tag.includes(type))
        const namePrompt = {
            type: 'input',
            name: 'projectName',
            default: '',
            message: `请输入${title}名称`,
            validate: function(v) {
                const done = this.async()
                setTimeout(function() {
                    // 1. 首字符必须为因为字母
                    // 2. 尾字符必须为英文或数字，不能为字符
                    // 3. 字符仅仅允许 -_
                    // 合法名称例如： a, a1, a-b, a_b, a-b-c, a_b_c, a1-b1-c1, a1_b1_c1
                    if (!isValidName(v)) {
                        done(`请输入合法的${title}名称`)
                        return
                    }
                    done(null, true)
                }, 0)
            },
            filter: function(v) {
                return v
            }
        }
        const questions = []
        if (!isValidName(this.projectName)) {
            questions.push(namePrompt)
        }
        questions.push({
            type: 'input',
            name: 'projectVersion',
            default: '1.0.0',
            message: `请输入${title}版本号`,
            validate: function(v) {
                const done = this.async()
                setTimeout(function() {
                    if (!(!!semver.valid(v))) {
                        done('请输入合法的版本号')
                        return
                    }
                    done(null, true)
                }, 0)
                return 
            },
            filter: v => !!semver.valid(v) ? semver.valid(v) : v
        }, {
            type: 'list',
            name: 'projectTemplate',
            message: `请选择${title}模板`,
            choices: this.createTemplateChoices()
        })
        if (type === TYPE_PROJECT) {
            const project = await inquirer.prompt(questions)
            projectInfo = {
                projectName: this.projectName,
                type,
                ...project
            }
        } else if (type === TYPE_COMPONENT) {
            const descriptionPrompt = {
                type: 'input',
                name: 'componentDescription',
                message: '请输入组件描述信息',
                default: '',
                validate: function(v) {
                    const done = this.async()
                    setTimeout(function() {
                        if (!v) {
                            done('请输入组件描述信息')
                            return
                        }
                        done(null, true)
                    }, 0)
                    return 
                }
            }
            questions.push(descriptionPrompt)
            // 获取组件的基本信息
            const component = await inquirer.prompt(questions)
            projectInfo = {
                projectName: this.projectName,
                type,
                ...component
            }
        }
        if (projectInfo.projectName) {
            projectInfo.className = require('kebab-case')(projectInfo.projectName).replace(/^-/, '')
        }
        if (projectInfo.projectName) {
            projectInfo.name = projectInfo.projectName
        }
        if (projectInfo.projectVersion) {
            projectInfo.version = projectInfo.projectVersion
        }
        if (projectInfo.componentDescription) {
            projectInfo.description = projectInfo.componentDescription
        }
        // 2. 获取项目的基本信息
        return projectInfo
    }

    async downLoadTemplate() {
        // 1. 通过项目模板API获取项目模板信息
        const { projectTemplate } = this.projectInfo
        const templateInfo = this.template.find(item => item.npmName === projectTemplate)
        const targetPath = path.resolve(userHome, '.zy-cli', 'template')
        const storeDir = path.resolve(userHome, '.zy-cli', 'template', 'node_modules')
        const { npmName, version } = templateInfo
        this.templateInfo = templateInfo
        const templateNpm = new Package({
            targetPath,
            storeDir,
            packageName: npmName,
            packageVersion: version
        })
        this.templateNpm = templateNpm
        if (!await templateNpm.exists()) {
            const spinner = spinnerInit('正在下载模板...')
            await sleep()
            try {
                await templateNpm.install()
            } catch (e) {
                throw e
            } finally {
                spinner.stop(true)
                if (await templateNpm.exists()) {
                    log.success('下载模板成功')
                }
            }
        } else {
            const spinner = spinnerInit('正在更新模板...')
            await sleep()
            try {
                await templateNpm.update()
            } catch (e) {
                throw e
            } finally {
                spinner.stop(true)
                if (await templateNpm.exists()) {
                    log.success('更新模板成功')
                }
            }
        }

    }

    createTemplateChoices() {
        return this.template.map(item => ({
            value: item.npmName,
            name: item.name
        }))
    }

    ejsRender(options) {
        const projectInfo = this.projectInfo
        const dir = process.cwd()
        return new Promise((resolve, reject) => {
            glob('**', {
                cwd: dir,
                nodir: true,
                ignore: options.ignore
            }, (err, files) => {
                if (err) reject(err)
                Promise.all(files.map(file => {
                    const filePath = path.join(dir, file)
                    return new Promise((rs, rj) => {
                        ejs.renderFile(filePath, projectInfo, (err, res) => {
                            if (err) rj(err)
                            fs.writeFileSync(filePath, res)
                            rs(res)
                        })
                    })
                })).then(resolve).catch(reject)
            })
        })
    }

    checkCommand(command) {
        return WHITE_COMMAND.includes(command) ? command : null
    }

    isDirEmpty(localPath) {
        let fileList = fs.readdirSync(localPath)
        fileList = fileList.filter(file => !file.startsWith('.') || ['node_modules'].includes(file))
        return !fileList || !fileList.length
    }
}

function init(argv) {
    return new InitCommand(argv)
}

module.exports = init
